Skip to content

New feature: Happy Eyeballs (RFC 8305) #4667

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 7, 2025
Merged

New feature: Happy Eyeballs (RFC 8305) #4667

merged 7 commits into from
Jun 7, 2025

Conversation

patterniha
Copy link
Contributor

@patterniha patterniha commented Apr 29, 2025

when using "AsIs" mode, golang controls how to connect to a "domain" address.

currently, golang use RFC-6555 to connect to the domains, although it is obsolete, but it is better than nothing. it solve the problem when one IP-type is unreachable. but regardless of the IP type, some IPs may not be available, thus RFC 8305 was created #4473 (comment)

RFC 6555 is obsoleted by RFC 8305, and python use RFC-8305 from v3.8.

///

anyway, It doesn't matter what RFC is implemented for the "AsIs" mode, because we should use built-in-dns to bypass GFW DNS spoofing,
and for "useIP/forceIP" mode, we don't even have RFC-6555, and only one random IP is selected.

and also Iran-GFW blocked some range-IPs of meta(facebook, instagram,...) and some not, so with happy-eyeballs iranian-users can access instagram without using any server.(in 99% cases)

as a result, I implemented Happy Eyeballs RFC-8305 for "forceIP/useIP" mode.

///

because happy eyeballs only applies when sockopt-domainStrategy is "forceIP/useIP" and this options are in "sockopt" settings, so i put "happyEyeballs" settings in "sockopt" settings.

///

we can use happy eyeballs for all type of proxy: freedom, vless, ...

///

"streamSettings": {
  "sockopt": {
    "domainStrategy": "forceIP",
      "happyEyeballs": {
              "prioritizeIPv6": false, 
              "maxConcurrentTry": 4,
              "tryDelayMs": 250,
              "interleave": 1
       }
     }
   } 

///

prioritizeIPv6 ( bool): indicate "First Address Family" in RFC-8305, default is false(= prioritizeIPv4)

interleave (uint32): indicate "First Address Family count" in RFC-8305, default is 1

maxConcurrentTry (uint32): maximum concurrent attempt (this is only maximum and in most cases our concurrent attempts is less, unless all connection fail to connect) also we can always have a maximum of concurrent-attempt as many IPs as we have, and this option is useful when the number of IPs is too high, and we want to control the number of concurrent-attempts, default is 4. if it is 0, happy-eyeballs is disabled.

tryDelayMs: delay time between each attempt, RFC-8305 recommend 250ms, so the default is 250. if it is 0, happy-eyeballs is disabled.

///

for example suppose our IP-list is [ip4-1, ip4-2, ip4-3, ip4-4, ip6-1, ip6-2, ip6-3, ip6-4]

when interleave is 1 and prioritizeIPv6 is false, the sorted-ip-list is:
[ip4-1, ip6-1, ip4-2, ip6-2, ip4-3, ip6-3, ip4-4, ip6-4]

and when for example interleave is 2 and prioritizeIPv6 is true:
[ip6-1, ip6-2, ip4-1, ip4-2, ip6-3, ip6-4, ip4-3, ip4-4]

then delay 250ms for each attempt until first connection is established.

the first-stablished-connection is winner connection and selected for sending/receiving data.

///

update:

happy-eyeballs is disabled by default, so you need to set tryDelayMs > 0 to enable.(recommend value is 250)

@Fangliding
Copy link
Member

Fangliding commented Apr 29, 2025

最好只留一个delay,大于0代表启用,别的取默认就行了。结构体套娃最好少一点。maxConcurrentTry 4够了 算上 v6 8也够了,或者智能一点,单栈取4双栈取8,它的主要作用是遇到奇葩域名别一次性dial出去几十个连接。interleave选1就好。prioritizeIPv6字段没必要。prefer从前面的domainstrategy获取,比如 ForceIPv4v6 就是 prefer v4
golang在单栈机器上dial不支持的IP类型失败会快速返回一个unreachable,可以借此快速开启下一轮重试并在以后跳过不支持的ipversion(不干保持简洁也行,用户自己配的prefer弄错了是他们自己的问题)
<-time.After(waitTime) 这样会导致每次进result之后等待时间重置。用time.Ticker就可以了,也不用很奇怪的设置为等待100小时了,直接Stop()就行

@patterniha
Copy link
Contributor Author

patterniha commented Apr 29, 2025

@Fangliding

first, thanks for your feedback and reviewing my code.

  1. we have to set domainStrategy to forceIP to have both IPv4 and IPv6, so we can't delete prioritizeIPv6 option (if we set ForceIPv4v6 we only have IPv4, so we can't try IPv6, unless the domain does not have IPv4!)

    ips, _, err := dnsClient.LookupIP(domain, dns.IPOption{
    IPv4Enable: (localAddr == nil || localAddr.Family().IsIPv4()) && strategy.preferIP4(),
    IPv6Enable: (localAddr == nil || localAddr.Family().IsIPv6()) && strategy.preferIP6(),
    })

  2. interleave option is "First Address Family Count" in RFC-8305 and even in python we can customize that:
    https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.create_connection
    also, suppose we want to try all IPv6 before IPv4, so we can set interleave to a large value for example 100, and then we make sure all the IPv6 are tested before the IPv4, so we need interleave option.

  3. although maxConcurrentTry = 4 is enough for most cases, but we may need to increase that in some special cases (for example finding a healthy IP from a bunch of IPs), so it is better to keep that.(also, i want to increase that in serverless-for-Iran)

  4. we can't remove enabled option, because the default value for tryDelayMs is 250, so if a user omits to set this option, we should set it to 250, but golang json.Unmarshal does not distinguish between "not setting tryDelayMs option" and "setting tryDelayMs = 0".

  5. You're right, "wait for 100 hours" was weird, but we need Timer instead of Ticker in this algorithm, so i use Timer instead of time.After, thx.

  6. I enable happy-eyeballs by default in new commit, because in AsIs mode happy-eyeballs is enabled by default (although it is still RFC-6555), also, this is a useful option that all users should take advantage of.

@Fangliding
Copy link
Member

Fangliding commented Apr 30, 2025

@Fangliding

first, thanks for your feedback and reviewing my code.

  1. we have to set domainStrategy to forceIP to have both IPv4 and IPv6, so we can't delete prioritizeIPv6 option (if we set ForceIPv4v6 we only have IPv4, so we can't try IPv6, unless the domain does not have IPv4!)
    ips, _, err := dnsClient.LookupIP(domain, dns.IPOption{
    IPv4Enable: (localAddr == nil || localAddr.Family().IsIPv4()) && strategy.preferIP4(),
    IPv6Enable: (localAddr == nil || localAddr.Family().IsIPv6()) && strategy.preferIP6(),
    })

只查v4/6是之前结构下的无奈之举 因为只能随机roll一个IP出来 在这种情况下可以手动两种都查

  1. interleave option is "First Address Family Count" in RFC-8305 and even in python we can customize that:
    https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.create_connection
    also, suppose we want to try all IPv6 before IPv4, so we can set interleave to a large value for example 100, and then we make sure all the IPv6 are tested before the IPv4, so we need interleave option.

python选项里有不代表要搬到golang的xray的config里来 我实在看不到这有啥配置的必要

  1. although maxConcurrentTry = 4 is enough for most cases, but we may need to increase that in some special cases (for example finding a healthy IP from a bunch of IPs), so it is better to keep that.(also, i want to increase that in serverless-for-Iran)

很多时候一个域名下面应该不会存在那么多的IP 大于4个的A记录和AAAA都挺少见

  1. we can't remove enabled option, because the default value for tryDelayMs is 250, so if a user omits to set this option, we should set it to 250, but golang json.Unmarshal does not distinguish between "not setting tryDelayMs option" and "setting tryDelayMs = 0".

设置为0禁用或者启用一个功能很常见 如果真的想快速发出一堆请求 设置为1并没有什么不同

说这么多主要目的还是为了压缩到只剩一个选项 当然只是我觉得这样更好

@patterniha
Copy link
Contributor Author

I explained the necessity of each of the options.
Xray-core is CORE, It should cover all types of uses.

@RPRX
Copy link
Member

RPRX commented Apr 30, 2025

看起来这个 PR 还在讨论中,所以四月累积更新版本暂不包含这个 PR

@patterniha
Copy link
Contributor Author

patterniha commented Apr 30, 2025

It seems that this PR is still under discussion, so the April cumulative update version does not include this PR for the time being.

@RPRX

Even @Fangliding accepted that.

But he says to remove the extra-options and I explained to him why these options are needed, there is no other difference.

Changing happy-eyeballs default parameters is useful for serverless-for-Iran (my main goal) and other goals.

So please merge it as soon as possible, I have written these for almost a month.
And i need these two PRs for serverless-for-Iran-anti-sanction-version.

@patterniha
Copy link
Contributor Author

patterniha commented Apr 30, 2025

@RPRX

happy-eyeballs is a feature that most users don't need to know about, It is enabled by default for everyone and increases the quality of their Internet usage.

They don't need to change anything in the configuration.

It is true that the default values ​​are sufficient for most users, but in many cases they need to be changed, so the existence of these options is essential.

@RPRX
Copy link
Member

RPRX commented May 1, 2025

如果 Chrome 已经有了这样的行为则 Xray 可以默认改成一样的行为

@patterniha
Copy link
Contributor Author

patterniha commented May 1, 2025

@RPRX

If you are worried about Detection by firewall and different behavior
Even golang-built-in-happy-eyeballs behaves differently with Chrome.
So you should even remove AsIs mode.

Each connection Is independent and each connection behavior is similar to Chrome.

So there is no need to worry about this.

@RPRX
Copy link
Member

RPRX commented May 1, 2025

只是 Xray 不应出现过于独特的行为,要么 Golang 要么 Chrome 要么非默认

@patterniha
Copy link
Contributor Author

patterniha commented May 1, 2025

@RPRX

I implemented rfc-8305 exactly, and this is exactly python happy-eyeballs.

So the behavior is exactly Python's behavior.

@patterniha
Copy link
Contributor Author

patterniha commented May 1, 2025

@RPRX

https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.create_connection

I already looked at the Python codes, and my happy-eyeballs is exactly python's happy-eyeballs.

@patterniha
Copy link
Contributor Author

patterniha commented May 9, 2025

@Fangliding

i remove enabled option and if tryDelayMs or maxConcurrentTry is 0, happy-eyeballs is disabled.

any other questions?

@xqzr
Copy link
Contributor

xqzr commented Jun 3, 2025

我尝试了...

{
 "protocol": "freedom",
 "settings": {
  "domainStrategy": "ForceIP"
 }
}

这将不起作用😇

@patterniha
Copy link
Contributor Author

patterniha commented Jun 3, 2025

@xqzr

To make happy-eyeballs work for all types of proxies we need to implement happy-eyeballs in the transport-layer(dialer).

But freedom-domainStrategy is applied before dialer and it replaces the domain with a random IP before dialer, so in dialer we only have an IP instead of a domain and we can't use happy-eyeballs.

So for using happy-eyeballs you need to use sockopt-domainStrategy which is applied in dialer.

"streamSettings": {
        "sockopt": {
                "domainStrategy": "ForceIP"
        }
}

///

freedom-domainStrategy is just a redundant option for sockopt-domainStrategy, and I think it should be removed.

Because firstly, it does exactly the same thing as sockopt-domainStrategy, and secondly, if we use it, it will prevent us from using happy-eyeballs.

@xqzr
Copy link
Contributor

xqzr commented Jun 3, 2025

So for using happy-eyeballs you need to use sockopt-domainStrategy which is applied in dialer.

没错!
这将为我工作

@RPRX
Copy link
Member

RPRX commented Jun 6, 2025

这个功能仍需更多讨论

@patterniha
Copy link
Contributor Author

patterniha commented Jun 6, 2025

@RPRX

OK let's discuss, I am ready to clear up any confusion, this is must-have feature for serverless-usage.

I hope this feature is added before the next version is released, so that I can release the complete serverless-for-iran-anti-sanction-config.

@xqzr
Copy link
Contributor

xqzr commented Jun 6, 2025

我认为,这个 PR 可以被合并

@RPRX
Copy link
Member

RPRX commented Jun 7, 2025

我觉得这个功能可以接受,但以当前参数默认启用的话会有独特的特征(即使它符合 Python),我更希望它符合 Chrome

所以今天的新版不会包含这个 PR,但这个月内的下一个新版应该会,还有 ECH,Vision Seed,REALITY 回落限速啥的

@patterniha
Copy link
Contributor Author

I think this feature is acceptable, but it would have unique characteristics if it was enabled by default with the current parameters (even if it complied with Python), and I would prefer it to complied with Chrome.

So today's new version will not include this PR, but the next new version within this month should include ECH, Vision Seed, REALITY speed limit and so on.

@RPRX

It's very easy for me to add Algorithm option:

"Algorithm": "RFC6555"/"RFC8305"

(Chrome and Go still use RFC6555)

but RFC 6555 is obsoleted by RFC 8305, and sooner or later Chrome and Go will have to update to RFC8305.

///

Although this option is enabled by default, but it is only active when the user has changed sockopt-domainStrategy, Which involves a small number of users and does not seem necessary except in serverless usage.

However, the default can be ‍disabled, but I don't recommend it because it is enabled by default in AsIs-mode (built-in-go-happy-eyeball).

/////////////////////////////////////////////////////////////////////

So, with this explanation, what changes need to be made to merge?

@RPRX
Copy link
Member

RPRX commented Jun 7, 2025

正好我也还在改新版本,你在线的话就加个 enabled bool 吧

@patterniha
Copy link
Contributor Author

patterniha commented Jun 7, 2025

I'm still working on a new version, so if you're online, I'll add a enabledbool.

the initial commit had enabled option, but i removed it due @Fangliding suggestion.

the tryDelayMs does not need to be 0, so when it is 0, happy-eyeballs is disabled.

so there is no need for enabled option.

///

you mean change the default value of tryDelayMs to 0 ?

@RPRX
Copy link
Member

RPRX commented Jun 7, 2025

可以,总之就是先不要搞成默认启用的就行

@patterniha
Copy link
Contributor Author

ok, wait.

@Meo597
Copy link
Contributor

Meo597 commented Jun 7, 2025

我觉得这个功能可以接受,但以当前参数默认启用的话会有独特的特征(即使它符合 Python),我更希望它符合 Chrome

所以今天的新版不会包含这个 PR,但这个月内的下一个新版应该会,还有 ECH,Vision Seed,REALITY 回落限速啥的

既然这个月会merge reality限速,那我就rebase相关PR了


然后,要预警一下因为docker有break changes,而且没法用代码做兼容
因此如果有人搞了自动更新可能会挂
他都搞自动更新了,解决这种问题也分分钟的事

不过我看下载量本来就没多少人用,搞自动更新的就更少了

@RPRX
Copy link
Member

RPRX commented Jun 7, 2025

@patterniha 我这边已经 ready 了,你那边大概需要多久

@patterniha
Copy link
Contributor Author

Half an hour at most, thx.

@patterniha
Copy link
Contributor Author

@RPRX

done.

@RPRX RPRX merged commit 97fdcb4 into XTLS:main Jun 7, 2025
39 checks passed
@RPRX
Copy link
Member

RPRX commented Jun 7, 2025

感谢 PR,可以去给 Severless-for-Iran PR 新版配置了,我等下先发个 v25.6.7 的 pre-release,明天再写 release notes

@patterniha
Copy link
Contributor Author

Thank you very much for caring about Iranian users.

///

The new config is available, but I have to test it on several ISPs, then I will update.

@patterniha patterniha deleted the happy branch June 7, 2025 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants